/*
*    l3qd - Light, light, lightweight queue
*    Copyright (C) 2024  Marcus Pedersén marcus@marcux.org
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package client

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"github.com/spf13/viper"
	"io"
	"math/rand"
	"net/http"
	"notabug.org/marcux/l3q/internal/common"
	"os"
	"path/filepath"
	"strings"
)

type Config struct {
	L3qdHost        string `mapstructure:"l3qd_host"`
	L3qdPort        uint   `mapstructure:"l3qd_port"`
	L3qValidateFile string `mapstructure:"l3q_validate_file"`
	L3qGroup        string `mapstructure:"l3q_group"`
}

// Reads and parse config file
// in success config is returned
// on failure error is printed
// and program exited.
func GetConfig(verbose bool) Config {
	home, err := os.UserHomeDir()
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to get user home directory:\n%s\n", err)
		} else {
			fmt.Fprintf(os.Stderr, "Failed to get user home directory.\n")
		}
		os.Exit(1)
	}

	exec, errEx := os.Executable()
	cfgFileBin := ""
	if errEx == nil {
		cfgFileBin = filepath.Dir(exec)
	}
	cfgFileHome := filepath.Join(home, ".l3q/")
	cfgFile := "/etc/l3q/"

	viper.SetConfigName("l3q.conf")
	viper.SetConfigType("toml")
	if len(cfgFileBin) > 0 {
		viper.AddConfigPath(cfgFileBin)
	}
	viper.AddConfigPath(cfgFileHome)
	viper.AddConfigPath(cfgFile)
	viper.SetDefault("l3qd_port", 39911)
	viper.SetDefault("l3q_validate_file", "/etc/l3q/network.l3q")
	viper.SetDefault("l3q_group", "l3q")

	if err := viper.ReadInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
			if verbose {
				fmt.Fprintf(os.Stderr, "Config file: l3q.conf not found.\nIn neither of valid locations:\n")
				fmt.Fprintf(os.Stderr, "%s\n%s\n%s\nCreate and configure config file.\n", cfgFileHome, cfgFile, cfgFileBin)
			} else {
				fmt.Fprintf(os.Stderr, "Config file: l3q.conf not found.\nCreate and configure config file.\n")
			}
		} else {
			if verbose {
				fmt.Fprintf(os.Stderr, "Failed to read and parse config file: %s\n%s\n", viper.ConfigFileUsed(), err)
			} else {
				fmt.Fprintf(os.Stderr, "Failed to read and parse config file: %s\n", viper.ConfigFileUsed())
			}
		}
		os.Exit(1)
	}

	var conf Config
	err = viper.Unmarshal(&conf)
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to decode config file: %s into struct,\n%s\n", viper.ConfigFileUsed(), err)
		} else {
			fmt.Fprintf(os.Stderr, "Failed to decode config file: %s\n", viper.ConfigFileUsed())
		}
		os.Exit(1)
	}

	return conf
}

// Returns a client that
// will not verify server
// certificate.
func SkipVerifyHttpClient() *http.Client {
	return &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
			},
		},
	}
}

// Returns the validate key that
// has to be supplied as an argument
// to server to accept request
func GetValidateKey(filePath string, verbose bool) string {
	byteKey, err := os.ReadFile(filePath)
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to read validate file: %s, %s", filePath, err)
		} else {
			fmt.Fprintf(os.Stderr, "Failed to read validate file: %s", filePath)
		}
		os.Exit(1)
	}

	return strings.TrimSpace(string(byteKey))
}

// Appends key to supplied url
func AppendKeyToUrl(key string, url string) string {
	var keyUrl string

	if strings.Contains(url, "?") {
		if strings.HasSuffix(url, "/") {
			keyUrl = fmt.Sprintf("%s&key=%s", url[:len(url)-1], key)
		} else {
			keyUrl = fmt.Sprintf("%s&key=%s", url, key)
		}
	} else {
		if strings.HasSuffix(url, "/") {
			keyUrl = fmt.Sprintf("%s?key=%s", url[:len(url)-1], key)
		} else {
			keyUrl = fmt.Sprintf("%s?key=%s", url, key)
		}
	}

	return keyUrl
}

// Makes a GET request to
// host specified in config.
// urlSuffix is the specific
// request sent to API.
// If verbose is specified
// detailed prints will be
// made to stdout.
func UrlGet(urlSuffix string, verbose bool) []byte {
	var conf = GetConfig(verbose)
	var baseUrl = fmt.Sprintf("https://%s:%d/api/v1/client", conf.L3qdHost, conf.L3qdPort)
	var url string

	if strings.HasPrefix(urlSuffix, "/") {
		url = baseUrl + urlSuffix
	} else {
		url = baseUrl + "/" + urlSuffix
	}

	key := GetValidateKey(conf.L3qValidateFile, verbose)
	keyUrl := AppendKeyToUrl(key, url)

	client := SkipVerifyHttpClient()
	resp, err := client.Get(keyUrl)
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to connect to server: %s\n%s\n", url, err)
		} else {
			fmt.Fprintf(os.Stderr, "Failed to connect to server: %s\n", url)
		}
		os.Exit(1)
	}

	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		if verbose {
			fmt.Fprintf(os.Stderr, "Error in response from server: %s\n%s\n", url, resp.Status)
		} else {
			fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
		}
		os.Exit(1)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to read body in response from server: %s\n%s\n", url, err)
		} else {
			fmt.Fprintf(os.Stderr, "Failed to read body in response from server: %s\n", url)
		}
		os.Exit(1)
	}

	return body
}

// Makes a PATCH request to
// host specified in config.
// urlSuffix is the specific
// request sent to API
// If verbose is specified
// detailed prints will be
// made to stdout.
func UrlPatch(urlSuffix string, verbose bool) {
	var conf = GetConfig(verbose)
	var baseUrl = fmt.Sprintf("https://%s:%d/api/v1/client", conf.L3qdHost, conf.L3qdPort)
	var url string

	if strings.HasPrefix(urlSuffix, "/") {
		url = baseUrl + urlSuffix
	} else {
		url = baseUrl + "/" + urlSuffix
	}

	key := GetValidateKey(conf.L3qValidateFile, verbose)
	keyUrl := AppendKeyToUrl(key, url)

	httpClient := SkipVerifyHttpClient()
	req, err := http.NewRequest(http.MethodPatch, keyUrl, bytes.NewBuffer([]byte{}))
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to create PATCH request: %s\n%s\n", url, err)
		} else {
			fmt.Fprintf(os.Stderr, "Failed to create PATCH request: %s\n", url)
		}
		os.Exit(1)
	}

	resp, err := httpClient.Do(req)
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to connect to server: %s\n%s\n", url, err)
		} else {
			fmt.Fprintf(os.Stderr, "Failed to connect to server: %s\n", url)
		}
		os.Exit(1)
	}

	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		if verbose {
			fmt.Fprintf(os.Stderr, "Error in response from server: %s\n%s\n", url, resp.Status)
		} else {
			fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
		}

		body, err := io.ReadAll(resp.Body)
		if err != nil {
			if verbose {
				fmt.Fprintf(os.Stderr, "Failed to read body in response from server: %s\n%s\n", url, err)
			} else {
				fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
			}

			os.Exit(1)
		}

		if len(body) > 0 {
			failResp := common.FailResponse{}

			err := json.Unmarshal(body, &failResp)
			if err != nil {
				if verbose {
					fmt.Fprintf(os.Stderr, "Failed to parse error response from server:\n%s\n", err)
				} else {
					fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
				}

				os.Exit(1)
			}

			if verbose {
				fmt.Fprintf(os.Stderr, "Server response error:\n%s\n", failResp.Message)
			} else {
				fmt.Fprintf(os.Stderr, "%s\n", failResp.Message)
			}
		} else if !verbose {
			fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
		}

		os.Exit(1)
	}
}

// Makes a POST request to
// host specified in config.
// urlSuffix is the specific
// request sent to API
// If verbose is specified
// detailed prints will be
// made to stdout.
// Returns id of newly created jobOB
func UrlPostJob(urlSuffix string, verbose bool, job common.Job) uint {
	var conf = GetConfig(verbose)
	var baseUrl = fmt.Sprintf("https://%s:%d/api/v1/client", conf.L3qdHost, conf.L3qdPort)
	var url string

	if strings.HasPrefix(urlSuffix, "/") {
		url = baseUrl + urlSuffix
	} else {
		url = baseUrl + "/" + urlSuffix
	}

	jsonJob, err := json.Marshal(job)
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to convert job to json: %s\n", err)
		} else {
			fmt.Fprintf(os.Stderr, "Failed to convert job to json\n")
		}

		os.Exit(1)
	}

	key := GetValidateKey(conf.L3qValidateFile, verbose)
	keyUrl := AppendKeyToUrl(key, url)

	client := SkipVerifyHttpClient()
	resp, err := client.Post(keyUrl, "application/json", bytes.NewBuffer(jsonJob))
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to connect to server: %s\n%s\n", url, err)
		} else {
			fmt.Fprintf(os.Stderr, "Failed to connect to server: %s\n", url)
		}
		os.Exit(1)
	}

	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		if verbose {
			fmt.Fprintf(os.Stderr, "Failed to read body in response from server: %s\n%s\n", url, err)
		} else {
			fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
		}

		os.Exit(1)
	}

	if resp.StatusCode != 200 {
		if verbose {
			fmt.Fprintf(os.Stderr, "Error in response from server: %s\n%s\n", url, resp.Status)
		} else {
			fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
		}

		if len(body) > 0 {
			failResp := common.FailResponse{}

			err := json.Unmarshal(body, &failResp)
			if err != nil {
				if verbose {
					fmt.Fprintf(os.Stderr, "Failed to parse error response from server:\n%s\n", err)
				} else {
					fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
				}

				os.Exit(1)
			}

			if verbose {
				fmt.Fprintf(os.Stderr, "Server response error:\n%s\n", failResp.Message)
			} else {
				fmt.Fprintf(os.Stderr, "%s\n", failResp.Message)
			}
		} else if !verbose {
			fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
		}

		os.Exit(1)
	}

	if len(body) > 0 {
		idResp := common.IdResponse{}

		err := json.Unmarshal(body, &idResp)
		if err != nil {
			if verbose {
				fmt.Fprintf(os.Stderr, "Failed to parse id response from server:\n%s\n", err)
			} else {
				fmt.Fprintf(os.Stderr, "Error in response from server: %s\n", url)
			}

			os.Exit(1)
		}

		return idResp.Id
	}

	return 0
}

// If no name is specified for job
// this function will generate
// a random name built on two words
func GenerateJobName() string {
	firstWord := []string{
		"Big",
		"Strong",
		"Nice",
		"Cute",
		"Angry",
		"Quiet",
		"Shy",
		"Lame",
		"Calm",
		"Upset",
		"Smiling",
		"Crying",
		"Sobbing",
		"Carring",
		"Sleeping",
	}

	secondWord := []string{
		"Cat",
		"Animal",
		"Lion",
		"Tiger",
		"Monster",
		"Eagle",
		"Flower",
		"Tree",
		"Car",
		"Bike",
		"Horse",
		"Mouse",
		"River",
		"Bird",
		"Dino",
		"Snake",
		"Cobra",
		"Hamster",
		"Cow",
	}

	name := firstWord[rand.Intn(len(firstWord))]
	name = fmt.Sprintf("%s%s", name, secondWord[rand.Intn(len(secondWord))])

	return name
}

// Scales bytes to k, M, G, T
// Return value as formated string
func ScaleHuman(bytes uint64) string {
	noSuffix := 0
	scaleBytes := float64(bytes)

	for noSuffix < 4 {
		if scaleBytes/1024.0 > 1.0 {
			scaleBytes = scaleBytes / 1024.0
			noSuffix++
		} else {
			break
		}
	}

	switch noSuffix {
	case 0:
		return fmt.Sprintf("%.2f B", scaleBytes)
	case 1:
		return fmt.Sprintf("%.2f kB", scaleBytes)
	case 2:
		return fmt.Sprintf("%.2f MB", scaleBytes)
	case 3:
		return fmt.Sprintf("%.2f GB", scaleBytes)
	case 4:
		return fmt.Sprintf("%.2f TB", scaleBytes)
	default:
		return ""
	}
}
